上手 koa2 MySQL 实战开发|博客系列
# 学习目标
学完这篇教程,你将学会:
- 如果编写 Koa 中间件
- 通过 @koa/router 实现路由配置
- 通过 TypeORM 连接和读写 MySQL 数据库(其他数据库都类似)
- 了解 JWT 鉴权的原理,并动手实现
- 掌握 Koa 的错误处理机制
# 准备初始代码
我们已经为你准备好了项目的脚手架,运行以下命令克隆我们的初始代码:
git clone https://github.com/poetries/koa-quickstart.git
如果你访问 GitHub 不流畅,可以克隆我们的 Gitee 仓库:
git clone https://github.com/poetries/koa-quickstart.git
然后进入项目,安装依赖:
cd koa-quickstart && npm install
这里我使用了 package-lock.json 确保所有依赖版本一致,如果你用 yarn 安装依赖出现问题,建议删除 node_modules ,重新用 npm install 安装。
# 最简单的 Koa 服务器
创建
src/server.ts,编写第一个 Koa 服务器,代码如下:
// src/server.ts
import Koa from 'koa';
import cors from '@koa/cors';
import bodyParser from 'koa-bodyparser';
// 初始化 Koa 应用实例
const app = new Koa();
// 注册中间件
app.use(cors());
app.use(bodyParser());
// 响应用户请求
app.use((ctx) => {
ctx.body = 'Hello Koa';
});
// 运行服务器
app.listen(3000);
整个流程与一个基本的 Express 服务器几乎完全一致:
- 初始化应用实例 app
- 注册相关的中间件(跨域 cors 和请求体解析中间件 bodyParser)
- 添加请求处理函数,响应用户请求
- 运行服务器
- 定睛一看,第 3 步中的请求处理函数(Request Handler)好像不太一样。在 Express 框架中,一个请求处理函数一般是这样的:
function handler(req, res) {
res.send('Hello Express');
}
两个参数分别对应请求对象(Request)和响应对象(Response),但是在 Koa 中,请求处理函数却只有一个参数 ctx (Context,上下文),然后只需向上下文对象写入相关的属性即可(例如这里就是写入到返回数据 body 中)
function handler(ctx) {
ctx.body = 'Hello Koa';
}
# 运行服务器
我们通过 npm start 就能开启服务器了。可以通过 Curl (或者 Postman 等)来测试我们的 API:
$ curl localhost:3000
Hello Koa
# 大名鼎鼎的“洋葱模型”
你也许从来没有用过 Koa 框架,但很有可能听说过“洋葱模型”,而 Koa 正是洋葱模型的代表框架之一。下面这个图你也许很熟悉了:

不过以个人观点,这个图实在是太像“洋葱”了,反而不太好理解。接下来我们将以更清晰直观的方式来感受 Koa 中间件的设计之美。首先我们来看一下 Express 的中间件是什么样的:

请求(Request)直接依次贯穿各个中间件,最后通过请求处理函数返回响应(Response),非常简单。然后我们来看看 Koa 的中间件是什么样的:

可以看到,Koa 中间件不像 Express 中间件那样在请求通过了之后就完成了自己的使命;相反,中间件的执行清晰地分为「两个阶段」。我们马上来看下 Koa 中间件具体是什么样的。
# Koa 中间件的定义
Koa 的中间件是这样一个函数:
async function middleware(ctx, next) {
// 第一阶段
await next();
// 第二阶段
}
第一个参数就是
Koa Context,也就是上图中贯穿所有中间件和请求处理函数的绿色箭头所传递的内容,里面「封装了请求体和响应体」(实际上还有其他属性,但这里暂时不讲),分别可以通过 ctx.request 和 ctx.response 来获取,以下是一些常用的属性:
ctx.url // 相当于 ctx.request.url
ctx.body // 相当于 ctx.response.body
ctx.status // 相当于 ctx.response.status
- 中间件的第二个参数便是 next 函数,这个熟悉 Express 的同学一定知道它是干什么的:用来把控制权转交给下一个中间件。但是它跟 Express 的 next 函数本质的区别在于,「Koa 的 「「next」」 函数返回的是一个 Promise」,在这个 Promise 进入完成状态(Fulfilled)后,就会去执行中间件中第二阶段的代码。
- 那么我们不禁要问:这样把中间件的执行拆分为两个阶段,到底有什么好处吗?我们来通过一个非常经典的例子来感受一下:日志记录中间件(包括响应时间的计算)
# 实战:日志记录中间件
让我们来实现一个简单的日志记录中间件
logger,用于记录每次请求的方法、URL、状态码和响应时间。创建src/logger.ts,代码如下:
// src/logger.ts
import { Context } from 'koa';
export function logger() {
return async (ctx: Context, next: () => Promise<void>) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} ${ctx.status} - ${ms}ms`);
};
}
- 严格意义上讲,这里的 logger 是一个「中间件工厂函数」(Factory),调用这个工厂函数后返回的结果才是真正的 Koa 中间件。之所以写成一个工厂函数,是因为我们可以通过给工厂函数传参的方式来更好地控制中间件的行为(当然这里的 logger 比较简单,就没有任何参数)。
- 在这个中间件的第一阶段,我们通过 Date.now() 先获取请求进入的时间,然后通过 await next() 让出执行权,等待下游中间件运行结束后,再在第二阶段通过计算 Date.now() 的差值来得出处理请求所用的时间。
这里通过两个 Date.now() 之间的差值来计算运行时间其实是不精确的,为了获取更准确的时间,建议使用 process.hrtime() 。
然后我们在 src/server.ts 中把刚才的 logger 中间件通过 app.use 注册进去,代码如下:
// src/server.ts
// ...
import { logger } from './logger';
// 初始化 Koa 应用实例
const app = new Koa();
// 注册中间件
app.use(logger());
app.use(cors());
app.use(bodyParser());
// ...
这时候再访问我们的服务器(通过 Curl 或者其他请求工具),应该可以看到输出日志:

关于 Koa 框架本身的内容基本讲完了,但是对于一个比较完整的 Web 服务器来说,我们还需要更多的“武器装备”才能应对日常的业务逻辑。在接下来的部分,我们将通过社区的优秀组件来解决两个关键问题:路由和数据库,并演示如何结合 Koa 框架进行使用。
# 实现路由配置
由于 Koa 只是一个中间件框架,所以路由的实现需要独立的 npm 包。首先安装
@koa/router及其 TypeScript 类型定义:
$ npm install @koa/router
$ npm install @types/koa__router -D
有些教程使用 koa-router ,但由于 koa-router 目前处于几乎无人维护的状态,所以我们这里使用维护更积极的 Fork 版本 @koa/router
# 路由规划
在这篇教程中,我们将实现以下路由:
- GET /users :查询所有的用户
- GET /users/:id :查询单个用户
- PUT /users/:id :更新单个用户
- DELETE /users/:id :删除单个用户
- POST /users/login :登录(获取 JWT Token)
- POST /users/register :注册用户
# 实现 Controller
在 src 中创建 controllers 目录,用于存放控制器有关的代码。首先是 AuthController ,创建 src/controllers/auth.ts ,代码如下:
// src/controllers/auth.ts
import { Context } from 'koa';
export default class AuthController {
public static async login(ctx: Context) {
ctx.body = 'Login controller';
}
public static async register(ctx: Context) {
ctx.body = 'Register controller';
}
}
然后创建 src/controllers/user.ts,代码如下
// src/controllers/user.ts
import { Context } from 'koa';
export default class UserController {
public static async listUsers(ctx: Context) {
ctx.body = 'ListUsers controller';
}
public static async showUserDetail(ctx: Context) {
ctx.body = `ShowUserDetail controller with ID = ${ctx.params.id}`;
}
public static async updateUser(ctx: Context) {
ctx.body = `UpdateUser controller with ID = ${ctx.params.id}`;
}
public static async deleteUser(ctx: Context) {
ctx.body = `DeleteUser controller with ID = ${ctx.params.id}`;
}
}
注意到在后面三个 Controller 中,我们通过 ctx.params 获取到路由参数 id 。
# 实现路由
然后我们创建 src/routes.ts,用于把控制器挂载到对应的路由上面:
